import isPlainObject from 'lodash/isPlainObject'

let ObjProto = Object.prototype
let toString = ObjProto.toString
export const hasOwn = ObjProto.hasOwnProperty

let FN_MATCH_REGEXP = /^\s*function (\w+)/

// https://github.com/vuejs/vue/blob/dev/src/core/util/props.js#L159
export const getType = function getType (fn) {
  let type = fn !== null && fn !== undefined ? fn.type ? fn.type : fn : null
  let match = type && type.toString().match(FN_MATCH_REGEXP)
  return match && match[1]
}

export const getNativeType = function getNativeType (value) {
  if (value === null || value === undefined) return null
  let match = value.constructor.toString().match(FN_MATCH_REGEXP)
  return match && match[1]
}

/**
 * No-op function
 */
export const noop = function noop () { }

/**
 * Checks for a own property in an object
 *
 * @param {object} obj - Object
 * @param {string} prop - Property to check
 */
export const has = function has (obj, prop) {
  return hasOwn.call(obj, prop)
}

/**
 * Determines whether the passed value is an integer. Uses `Number.isInteger` if available
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger
 * @param {*} value - The value to be tested for being an integer.
 * @returns {boolean}
 */
export const isInteger = Number.isInteger || function (value) {
  return typeof value === 'number' && isFinite(value) && Math.floor(value) === value
}

/**
 * Determines whether the passed value is an Array.
 *
 * @param {*} value - The value to be tested for being an array.
 * @returns {boolean}
 */
export const isArray = Array.isArray || function (value) {
  return toString.call(value) === '[object Array]'
}

/**
 * Checks if a value is a function
 *
 * @param {any} value - Value to check
 * @returns {boolean}
 */
export const isFunction = function isFunction (value) {
  return toString.call(value) === '[object Function]'
}

/**
 * Adds a `def` method to the object returning a new object with passed in argument as `default` property
 *
 * @param {object} type - Object to enhance
 */
export const withDefault = function withDefault (type) {
  Object.defineProperty(type, 'def', {
    value: function value (def) {
      if (def === undefined && this.default === undefined) {
        delete this.default
        return this
      }
      if (!isFunction(def) && !validateType(this, def)) {
        warn(this._vueTypes_name + ' - invalid default value: "' + def + '"', def)
        return this
      }
      this.default = isArray(def) || isPlainObject(def)
        ? function () {
          return def
        }
        : def
      return this
    },

    enumerable: false,
    writable: false
  })
}
/**
 *  字段描述信息
 * @param {*} type - Object to enhance
 */
export const withDescription = function withDescription (type) {
  Object.defineProperty(type, 'des', {
    value: function value (des) {
      if (des === undefined) {
        return this
      }
      if (String.prototype.toString.call(des) === '[object String]') {
        warn(this._vueTypes_name + ' - invalid description value: "' + des + '"', des)
        return this
      }
      this.description = des
      return this
    },
    enumerable: false,
    writable: false
  })
}

/**
 * Adds a `require` getter returning a new object with `required: true` key-value
 *
 * @param {object} type - Object to enhance
 */
export const withRequired = function withRequired (type) {
  Object.defineProperty(type, 'require', {
    value: function value () {
      this.required = true
      return this
    },
    enumerable: false,
    writable: false
  })
}

/**
 * Adds `isRequired` and `def` modifiers to an object
 *
 * @param {string} name - Type internal name
 * @param {object} obj - Object to enhance
 * @returns {object}
 */
export const toType = function toType (name, obj) {
  Object.defineProperty(obj, '_vueTypes_name', {
    enumerable: false,
    writable: false,
    value: name
  })
  withRequired(obj)
  withDefault(obj)
  withDescription(obj)

  if (isFunction(obj.validator)) {
    obj.validator = obj.validator.bind(obj)
  }
  return obj
}

/**
 * Validates a given value against a prop type object
 *
 * @param {Object|*} type - Type to use for validation. Either a type object or a constructor
 * @param {*} value - Value to check
 * @param {boolean} silent - Silence warnings
 * @returns {boolean}
 */
export const validateType = function validateType (type, value) {
  if (value === undefined || value == null) {
    return true
  }
  let silent = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false

  let typeToCheck = type
  let valid = true
  let expectedType = void 0
  if (!isPlainObject(type)) {
    typeToCheck = { type: type }
  }
  let namePrefix = typeToCheck._vueTypes_name ? typeToCheck._vueTypes_name + ' - ' : ''

  if (hasOwn.call(typeToCheck, 'type') && typeToCheck.type !== null) {
    if (isArray(typeToCheck.type)) {
      valid = typeToCheck.type.some(function (type) {
        return validateType(type, value, true)
      })
      expectedType = typeToCheck.type.map(function (type) {
        return getType(type)
      }).join(' or ')
    } else {
      expectedType = getType(typeToCheck)

      if (expectedType === 'Array') {
        valid = isArray(value)
      } else if (expectedType === 'Object') {
        valid = isPlainObject(value)
      } else if (expectedType === 'String' || expectedType === 'Number' || expectedType === 'Boolean' || expectedType === 'Function') {
        valid = getNativeType(value) === expectedType
      } else {
        valid = value instanceof typeToCheck.type
      }
    }
  }

  if (!valid) {
    silent === false && warn(namePrefix + 'value "' + value + '" should be of type "' + expectedType + '"')
    return false
  }

  if (hasOwn.call(typeToCheck, 'validator') && isFunction(typeToCheck.validator)) {
    valid = typeToCheck.validator(value)
    if (!valid && silent === false) warn(namePrefix + 'custom validation failed')
    return valid
  }
  return valid
}

let warn = noop

if (process.env.NODE_ENV !== 'production') {
  let hasConsole = typeof console !== 'undefined'
  warn = function warn (msg) {
    if (hasConsole) {
      console.warn('[VueTypes warn]: ' + msg)
    }
  }
}

export { warn }